/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2021. All rights reserved.
 */

package autodispose.lifecycle;

import autodispose.AutoDisposePlugins;
import autodispose.OutsideScopeException;
import io.reactivex.rxjava3.annotations.Nullable;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.CompletableSource;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.functions.Consumer;
import io.reactivex.rxjava3.functions.Predicate;
import io.reactivex.rxjava3.functions.Supplier;

import java.util.Comparator;

/**
 * Utilities for dealing with {@link LifecycleScopeProvider}s. This includes factories for resolving
 * {@link Completable} representations of scopes, corresponding events, etc.
 */
public final class LifecycleScopes {
    private static final Comparator<Comparable<Object>> COMPARABLE_COMPARATOR = Comparable::compareTo;

    private LifecycleScopes() {
        throw new InstantiationError();
    }

    /**
     * Overload for resolving lifecycle providers that defaults to checking start and end boundaries
     * of lifecycles. That is, they will ensure that the lifecycle has both started and not ended.
     *
     * <p><em>Note:</em> This resolves the scope immediately, so consider deferring execution as
     * needed, such as using {@link Completable#defer(Supplier) defer}.
     *
     * @param provider the {@link LifecycleScopeProvider} to resolve.
     * @param <E>      the lifecycle event type
     * @return a resolved {@link CompletableSource} representation of a given provider
     * @throws OutsideScopeException if the {@link LifecycleScopeProvider#correspondingEvents()}
     *                               throws an {@link OutsideScopeException} during resolution.
     */
    public static <E> CompletableSource resolveScopeFromLifecycle(
            final LifecycleScopeProvider<E> provider) throws OutsideScopeException {
        return resolveScopeFromLifecycle(provider, true);
    }

    /**
     * Overload for resolving lifecycle providers allows configuration of checking "end" boundaries of
     * lifecycles. That is, they will ensure that the lifecycle has both started and not ended, and
     * otherwise will throw one of {@link LifecycleNotStartedException} (if {@link
     * LifecycleScopeProvider#peekLifecycle() peekLifecycle} returns {@code null}) or {@link
     * LifecycleEndedException} if the lifecycle is ended. To configure the runtime behavior of these
     * exceptions, see {@link AutoDisposePlugins}.
     *
     * <p><em>Note:</em> This resolves the scope immediately, so consider deferring execution as
     * needed, such as using {@link Completable#defer(Supplier) defer}.
     *
     * @param provider           the {@link LifecycleScopeProvider} to resolve.
     * @param isCheckEndBoundary whether or not to check that the lifecycle has ended
     * @param <E>                the lifecycle event type
     * @return resolved {@link CompletableSource} representation of a given provider
     * @throws OutsideScopeException if the {@link LifecycleScopeProvider#correspondingEvents()}
     *                               throws an {@link OutsideScopeException} during resolution.
     */
    public static <E> CompletableSource resolveScopeFromLifecycle(
            final LifecycleScopeProvider<E> provider, final boolean isCheckEndBoundary)
            throws OutsideScopeException {
        E lastEvent = provider.peekLifecycle();
        CorrespondingEventsFunction<E> eventsFunction = provider.correspondingEvents();
        if (lastEvent == null) {
            throw new LifecycleNotStartedException();
        }
        E endEvent;
        try {
            endEvent = eventsFunction.apply(lastEvent);
        } catch (OutsideScopeException e) {
            if (isCheckEndBoundary && e instanceof LifecycleEndedException) {
                Consumer<? super OutsideScopeException> handler =
                        AutoDisposePlugins.getOutsideScopeHandler();
                if (handler != null) {
                    try {
                        handler.accept(e);

                        // Swallowed the end exception, just silently dispose immediately.
                        return Completable.complete();
                    } catch (Throwable throwable) {
                        return Completable.error(throwable);
                    }
                }
                throw e;
            }
            return Completable.error(e);
        }
        return resolveScopeFromLifecycle(provider.lifecycle(), endEvent);
    }

    /**
     * Completable source returns comparator.
     *
     * @param lifecycle the stream of lifecycle events
     * @param endEvent  the target end event
     * @param <E>       the lifecycle event type
     * @return resolved {@link Completable} representation of given lifecycle, targeting the given
     * event
     */
    public static <E> CompletableSource resolveScopeFromLifecycle(
            Observable<E> lifecycle, final E endEvent) {
        @Nullable
        Comparator<E> comparator = null;
        if (endEvent instanceof Comparable) {
            /* noinspection unchecked */
            comparator = (Comparator<E>) COMPARABLE_COMPARATOR;
        }
        return resolveScopeFromLifecycle(lifecycle, endEvent, comparator);
    }

    /**
     * Comparator method
     *
     * @param lifecycle  the stream of lifecycle events
     * @param endEvent   the target end event
     * @param comparator an optional comparator for checking event equality.
     * @param <E>        the lifecycle event type
     * @return a resolved {@link Completable} representation of a given lifecycle, targeting the given
     * event
     */
    public static <E> CompletableSource resolveScopeFromLifecycle(
            Observable<E> lifecycle, final E endEvent, @Nullable final Comparator<E> comparator) {
        Predicate<E> equalityPredicate;
        if (comparator != null) {
            equalityPredicate = e -> comparator.compare(e, endEvent) >= 0;
        } else {
            equalityPredicate = e -> e.equals(endEvent);
        }
        return lifecycle.skip(1).takeUntil(equalityPredicate).ignoreElements();
    }
}
